#include <vector>
#include <iostream>
#include <fstream>
#include <complex>
#include "ocl.h"

#include "Utils.h"

#include <CL\cl.h>
using namespace std;

std::string OpenCL::getPath()
{
	#ifdef _WIN32
		char buffer[MAX_PATH];
	#ifdef UNICODE
		if(!GetModuleFileName(NULL, (LPWCH)buffer, sizeof(buffer)))
			throw std::string("GetModuleFileName() failed!");
	#else
		if(!GetModuleFileName(NULL, buffer, sizeof(buffer)))
			throw std::string("GetModuleFileName() failed!");
	#endif
		std::string str(buffer);
		/* '\' == 92 */
		int last = (int)str.find_last_of((char)92);
	#else
		char buffer[PATH_MAX + 1];
		ssize_t len;
		if((len = readlink("/proc/self/exe",buffer, sizeof(buffer) - 1)) == -1)
			throw std::string("readlink() failed!");
		buffer[len] = '\0';
		std::string str(buffer);
		/* '/' == 47 */
		int last = (int)str.find_last_of((char)47);
	#endif
		return str.substr(0, last + 1);
}

void OpenCL::getIds() {
	// Connect to a compute device
	this->err = clGetPlatformIDs(1, &this->cpPlatform, NULL);
	if (err != CL_SUCCESS) {
		throw string("Error: Failed to find a platform!");
	}
	// Get a device of the appropriate type
	this->err = clGetDeviceIDs(this->cpPlatform, this->devType, 1, &this->device_id, NULL);
	if (this->err != CL_SUCCESS) {
		throw string("Error: Failed to create a device group!");
	}
}




void OpenCL::getNames() {
	size_t size;
	char name [256];
	
	clGetDeviceInfo( device_id, CL_DEVICE_GLOBAL_MEM_CACHE_SIZE, sizeof(cl_ulong),		&globalCacheSize,NULL);
	clGetDeviceInfo( device_id, CL_DEVICE_GLOBAL_MEM_CACHELINE_SIZE, sizeof(cl_uint),	&globalCacheLineSize,NULL);
	clGetDeviceInfo( device_id, CL_DEVICE_GLOBAL_MEM_SIZE, sizeof(cl_ulong),			&globalMemSize,NULL);
	clGetDeviceInfo( device_id, CL_DEVICE_LOCAL_MEM_SIZE, sizeof(cl_ulong),				&localMemSize,NULL);
	clGetDeviceInfo( device_id, CL_DEVICE_MAX_CLOCK_FREQUENCY, sizeof(cl_uint),			&clockSpeed,NULL);
	clGetDeviceInfo( device_id, CL_DEVICE_MAX_COMPUTE_UNITS, sizeof(cl_uint),			&computeUnits,NULL);
	clGetDeviceInfo( device_id, CL_DEVICE_MAX_MEM_ALLOC_SIZE, sizeof(cl_ulong),			&maxMemAlloc,NULL);
	clGetDeviceInfo( device_id, CL_DEVICE_MAX_WORK_GROUP_SIZE, sizeof(size_t),			&maxWorkGroupSize,NULL);
	clGetDeviceInfo( device_id, CL_DEVICE_MAX_WORK_ITEM_DIMENSIONS, sizeof(cl_uint),	&maxItemDim,NULL);
	clGetDeviceInfo( device_id, CL_DEVICE_COMPILER_AVAILABLE, sizeof(cl_bool),			&device_compiler_ok,NULL);
	//clGetDeviceInfo( device_id, CL_DEVICE_MAX_WORK_ITEM_SIZES, sizeof(size_t*3),			&device_availble,NULL);		
	clGetDeviceInfo( device_id, CL_DEVICE_AVAILABLE, sizeof(cl_bool), &device_availble,NULL);

	clGetDeviceInfo( device_id, CL_DEVICE_VERSION, 256, name, &size);
	this->device_version = name;
	clGetDeviceInfo( device_id, CL_DEVICE_VENDOR, 256, name, &size);
	this->device_vendor = name;
	clGetDeviceInfo( device_id, CL_DEVICE_VENDOR_ID, 256, name, &size);
	this->device_vendor_id = name;
	clGetDeviceInfo( device_id, CL_DRIVER_VERSION, 256, name, &size);
	this->driver_version = name;
	clGetDeviceInfo( device_id, CL_DEVICE_NAME, 256, name, &size);
	this->device_name = name;	
}

std::string OpenCL::ToString()
{
	stringstream stream;
	stream <<
	"device_availble=\t\t" << device_availble << endl << 
	"device_compiler_ok=\t\t" << 	device_compiler_ok << endl << 
	"globalCacheSize=\t\t" << globalCacheSize << endl << 
	"globalCacheLineSize=\t\t" << globalCacheLineSize << endl << 
	"globalMemSize=\t\t" << globalMemSize << endl << 
	"localMemSize=\t\t" << localMemSize << endl << 
	"clockSpeed=\t\t" << 	clockSpeed << endl << 
	"computeUnits=\t\t" << computeUnits << endl << 
	"maxMemAlloc=\t\t" << maxMemAlloc << endl << 
	"maxWorkGroupSize=\t\t" << maxWorkGroupSize << endl << 
	"maxItemDim=\t\t" << 	maxItemDim << endl << 
	"device_version=\t\t" << 	device_version << endl << 
	"driver_version=\t\t" << 	driver_version << endl << 
	"device_vendor=\t\t" << device_vendor << endl << 
	"device_vendor_id=\t\t" << device_vendor_id << endl << 
	"device_name=\t\t" << device_name << endl;


	return stream.str();

}

void OpenCL::createProgram(const char *filePath, string name) {
	this->functionName = name;
	
	// Create a compute context
	this->context = clCreateContext(0, 1, &this->device_id, NULL, NULL, &this->err);
	if (!this->context) {
		throw string("Error: Failed to create a compute context!");
	}

	// Create a command commands
	this->commands = clCreateCommandQueue(this->context, this->device_id, 0, &this->err);
	if (!this->commands) {
		throw string("Error: Failed to create a command commands!");
	}
		
	std::string kernelName = std::string(filePath);
	std::string path = OpenCL::getPath();
	path.append(kernelName);
		
	std::ifstream t(kernelName);
	std::stringstream buffer;
	buffer << t.rdbuf();

	string src = buffer.str();
	const char * srcStr = src.c_str();

	// Create the compute program from the source buffer
	this->program = clCreateProgramWithSource(this->context, 1, (const char **) &srcStr, NULL, &this->err);
	if (!this->program) {
		throw string("Error: Failed to create compute program!");
	}

	// Build the program executable
	this->err = clBuildProgram(this->program, 1, &this->device_id, NULL, NULL, NULL);
	if (this->err != CL_SUCCESS) {
		std::string error = checkError(this->err);
		size_t len;
		char buffer[2048];
		stringstream output;
		output << error << ": " << endl;
		clGetProgramBuildInfo(this->program, this->device_id, CL_PROGRAM_BUILD_LOG, sizeof(buffer), buffer, &len);
		output << buffer << " " << endl;
		clGetProgramBuildInfo(this->program, this->device_id, CL_PROGRAM_BUILD_STATUS, sizeof(buffer), buffer, &len);
		output << buffer << " ";
		throw output.str() ;
	}

	// Create the compute kernel in the program
	this->kernel = clCreateKernel(this->program, name.c_str(), &this->err);
	if (!this->kernel || this->err != CL_SUCCESS) {
		throw string("Error: Failed to create compute kernel!");
	}
}

OpenCL::OpenCL(const char *source, string start, int type) {
	this->devType = type;
		
	this->getIds();
	this->getNames();

	this->createProgram(source, start);
}

OpenCL::~OpenCL() {
	vector<cl_mem>::iterator itr;
	for(itr = this->arg.begin(); itr < this->arg.end(); itr++) {
		clReleaseMemObject(*itr);
	}
	clReleaseProgram(this->program);
	clReleaseKernel(this->kernel);
	clReleaseCommandQueue(this->commands);
	clReleaseContext(this->context);
}



void OpenCL::execute(int count) {	

	// Get the maximum work group size for executing the kernel on the device
	this->err = clGetKernelWorkGroupInfo(this->kernel, this->device_id, CL_KERNEL_WORK_GROUP_SIZE, sizeof(this->local), &this->local, NULL);
	this->checkError(this->err);

	// Execute the kernel over the vector using the 
	// maximum number of work group items for this device

	this->global = count;
	this->local = (this->global < this->local) ? this->global: this->local;

	cout << endl << "Starting " << this->global << " threads out of " << this->local << endl;

	cout << this->functionName << " ( ";
	for ( int i = 0; i < this->args.size(); i++ )
	{
		cout << endl << "\t" << this->args[i];
	}
	cout << " ) " << endl;

	this->err = clEnqueueNDRangeKernel(this->commands, this->kernel, 1, NULL, &this->global, &this->local, 0, NULL, NULL);
	this->checkError(this->err);

	// Wait for all commands to complete
	this->err = clFinish(this->commands);
	this->checkError(this->err);
}

std::string OpenCL::checkError(int err) {
	switch(this->err) {
	case CL_SUCCESS:
		return "";
	case CL_BUILD_PROGRAM_FAILURE:
		return string("Build Program Failure");
	case CL_INVALID_PROGRAM_EXECUTABLE:
		return string("Invalid program executable");
	case CL_INVALID_COMMAND_QUEUE:
		return string("Invalid command queue");
	case CL_INVALID_KERNEL:
		return string("Invalid kernel");
	case CL_INVALID_CONTEXT:
		return string("Invalid context");
	case CL_INVALID_KERNEL_ARGS:
		return string("Invalid context");
	case CL_INVALID_WORK_DIMENSION:
		return string("Invalid work dimension");
	case CL_INVALID_WORK_GROUP_SIZE:
		return string("Invalid work group size");
	case CL_INVALID_WORK_ITEM_SIZE:
		return string("Invalid work item size");
	case CL_INVALID_GLOBAL_OFFSET:
		return string("Invalid global offset");
	case CL_OUT_OF_RESOURCES:
		return string("Out of resources");
	case CL_MEM_OBJECT_ALLOCATION_FAILURE:
		return string("Memory object allocation failure");
	case CL_INVALID_EVENT_WAIT_LIST:
		return string("Invalid event wait list");
	case CL_OUT_OF_HOST_MEMORY:
		return string("Out of memory");
	default:
		return string("Unknown error");
	}
}